Short Tutorial on Signals in Linux
Vahab Pournaghshband
Let's examine the case of power failure while a reliable process
is running. When the power cable is pulled out, the power
doesn't die out immediately. In fact, it takes a few milliseconds before
the power is completely gone. This reliable process may need to be notified
of such power failures to, for instance, save states before being forced to
exit. Let's examine the possible approaches to accomplish this:
-
A bit in the file "/dev/power" would indicate the power status.
In this approach, the reliable program periodically reads the file.
If it reads 1, then it means the power is still on and the program would
continue whatever it was doing. However, in case of reading 0, the process
realizes that the power is gone and it must exit within, say, 10ms. This
approach has two major disadvantages: (1) it requires all programs, that want
to be reliable, to poll, and, (2) to make this to work, the applications
have to incorporate this mechanism in their implementation.
-
Another approach would be reading from a pipe rather than a file. In this case, unlike the previous
approach that needed to check for a change of a bit at every time interval,
the process will hang until a character is written to the pipe, indicating a
power failure. Clearly, this solution suffers from major drawbacks, not to mention
the requirement for modification of all applications. In this approach the process
is blocked while waiting for a change of power state, so the application can not
execute any of its actual code. To fix this we need
multithreading. In other words,
a separate thread should be delegated to reading the file for a signal of power
failure, to ensure that the main thread is not blocked. But now the question is
that how would the waiting thread tell the main thread that there is a power failure?
-
As another approach, the kernel can save the entire RAM to the disk once it
realizes that the power failure has occurred. Then, later, when the system starts
again, the kernel would restore the RAM. This approach, however, is not practical,
since writing to disk is extremely slow, so it may take more time to save than the
system actually has left.
-
The winner approach is sending SIGPWR signal to all processes in case of power
failures. In this approach, the kernel signals the processes of such event, and it
leaves it up to the processes to do what they want to do with it.
Signal Menagerie
The following table enumerates some of the signals. All signals are defined in
signal.h .
Events |
Corresponding Signals |
Unusual Hardware Events |
SIGPWR : Power failure
|
Uncooperative Processes |
SIGINT : Terminal interrupt signal
|
Invalid Programs |
SIGILL : Illegal (bad) instruction
SIGFPE : Floating-point exception
SIGSEGV : Segmentation violation
SIGBUS : Bus error
|
I/O Errors |
SIGIO : Device is ready
SIGPIPE : Broken pipe
|
Child Process Died |
SIGCHLD : Child status has changed
|
User Signals |
SIGKILL : Kill processes
SIGSTOP : Stop processes for later resumption
SIGTSTP : Suspended processes
SIGUSR1 : User-defined signal 1
|
User Went Away |
SIGHUP : Controlling terminal is closed
|
Time Expiration |
SIGALRM : Alarm clock
|
How to Handle Signals?
Back to our power failure example, here is how the power failure signal is established and handled:
1 int main()
2 {
3 signal(SIGPWR, powerFailureHandler);
4 ...
5 }
6
7 void powerFailureHandler(int signum)
8 {
9
10 ...
11 }
The first line in main() establishes a handler for the SIGPWR
signals. The first argument to signal is an integer specifying what signal
is referring to, while the second argument is a function pointer type which points
to the signal handler.
In our example, the powerFailureHandler() is a signal handler.
A handler is a function that is executed asynchronously when a particular
signal arrives. Since it interrupts the normal flow of execution, it can be
called between any pair of instructions. If a handler is not defined for a
particular signal, a default handler is used. The only two signals for which
a handler cannot be defined are SIGKILL and SIGSTOP .
What is Safe to Do Inside a Signal Handler?
There are DO's and DON'T's when it comes to signal handlers. For instance,
calling certain functions, called non-reentrant,
could potentially lead
to havoc. An example of such functions is malloc() which allocates additional
memory on heap. Recall that signals are asynchronous function calls and could be
raised at any time. In the case of malloc, havoc can result for the process, if a
signal occurs in the middle of allocating additional memory using malloc() ,
because malloc usually maintains a linked list of all its allocated area and
it may have been in the middle of changing this list. Another example of
non-reentrant function calls inside signal handlers is getchar() which
reads a byte from standard input. In that case, the process could lead
into an inconsistent state if it was in the middle of dealing with stdio
buffer when the signal arrived. On the other hand, reentrant functions like
close() are safe to use in signal handlers.
How to Block Signals?
Sometimes we would benefit more by not having signals at all usually to avoid race conditions.
Blocking a signal means telling the operating system to hold it and deliver it later. Generally,
a program does not block signals indefinitely, it might as well ignore them by setting their actions
to SIG_IGN . One way to block signals is to use sigprocmask which its format is:
sigprocmask(int how,
sigset_t const * restrict set,
sigset_t const * restrict oset)
Where how is either of three values: SIG_BLOCK , SIG_UNBLOCK , SIG_SETMASK . The
first two values specify whether the signals in the new signal mask should be blocked
or not, while the last specifies that the new mask should replace the old mask. set and
oset that hold new and original masks are both types of sigset_t which is a bitmap that
reserves one bit per signal, indicating which signal(s) are blocked. The following code is
an example of blocking SIGHUP signal while performing the string copy.
1 sigset_t newMask, oldMask;
2 sigemptyset(&newMask);
3 sigemptyset(&oldMask);
4
5
6 sigaddset(&newMask, SIGHUP);
7
8 sigprocmask(SIG_BLOCK, &newMask, &oldMask);
9 strcpy(tmp_file,"/tmp/foo");
10
11 sigprocmask(SIG_SETMASK, &oldMask, NULL);
The code segment between the two sigprocmask -s is called
critical section in the operating system context.
Go Volatile on Variables:
Let's examine the following code:
1 int x;
2
3 int main()
4 {
5 x=0
6 ...
7 x=1
8 ...
9 }
x is defined as a global variable. It is first set to 0 and later in main() ,
its value is changed to 1 without involving x in any statement between these
two assignments. While the compiler is compiling this code, it replaces the
x=0 statement by x=1 and removes the x=1 in line 7, since it is smart enough
to realizes that there is no need for x=0 , for it is never used. This seemingly
fine observation could lead to undesired behavior if the associated signal for
the following signalHandler occurs between line 5 and 7:
1 void signalHandler(...)
2 {
3 if (x)
4 unlink("f");
5 }
In this case, an entirely different action would be taken by the process
if the compiler does the optimization at the compilation stage. This
problem is fixed by telling the compiler to avoid such optimization using
the volatile keyword. volatile is widely used in codes involving signals,
and can be seen as a warning for potential race conditions. The code is
then revised as follows:
1 int volatile x;
2
3 int main()
4 {
5 x=0
6 ...
7 x=1
8 ...
9 }
Based on Paul Eggert's Lectures on Operating Systems Principles
|